iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
自我挑戰組

Data Engineer 下班後偷學 AI系列 第 5

目標讓 LangChain 成為 Pokémon 大師

  • 分享至 

  • xImage
  •  

更新啦

今天的文章要來學習 LangChain 的 Models、Tools 和 Agents 的基本概念,並結合 PokéAPI 來打造一個可以分析寶可夢屬性剋制並推薦組隊建議的 Agent。

Models

LangChain 的 Models 整合了各家模型供應商,提供了統一的介面,好處是換 Providers 時程式不用跟著一起改,可以透過同一個 init_chat_model 來初始化不同家的模型,但 init_chat_model 只是一個入口,真正跟各家 API 溝通的是各自獨立的 integration 套件,要另外安裝,舉例來說你在 init_chat_model 中調用了 OpenAI 的模型,背後實際用到的還是 langchain_openai,所以兩種寫法其實都可以用。

1. init_chat_model

from langchain.chat_models import init_chat_model

model = init_chat_model(
    "openai:gpt-6-astra",
    api_key=...,
    base_url=...,
)

2. Provider-specific

from langchain_openai import ChatOpenAI

model = ChatOpenAI(
    model="gpt-6-astra",
    api_key=...,
    base_url=...,
)

Tools

LangChain 的 Tools 讓我們可以很方便地用 @tool 把普通的 Python function 註冊成 Agent 能使用的 Tool。@tool 會把 Function name 當成 Tool Name,利用 type hints 產生 input schema,再把 docstring 當成 Tool 的說明,讓模型知道這個 Tool 是做什麼的、什麼時候該用。

回到開頭主題,為了讓我的 LangChain Agent 成為寶可夢大師,我規劃了 4 個工具:

  1. 取得所有 Pokémon 的中英文名稱對照表 (第一世代)
  2. 取得指定 Pokémon 的基本資料
  3. 取得 Pokémon 屬性的剋制與弱點
  4. 取得指定屬性的 Pokémon 候選名單

在此之前先寫一個所有 Tools 共用的 API 端點。

import requests

POKEAPI_BASE_URL = "https://pokeapi.co/api/v2"

def pokeapi_get(endpoint: str) -> dict:
    """呼叫 PokéAPI,統一處理 HTTP error。"""

    try:
        response = requests.get(
            f"{POKEAPI_BASE_URL}/{endpoint}",
            timeout=10,
        )
        response.raise_for_status()
        return response.json()

    except requests.RequestException as e:
        return {
            "error": f"PokéAPI request failed: {str(e)}"
        }

Tool 1 - 取得所有 Pokémon 的中英文名稱對照表

取得寶可夢中文名稱:

def get_species_name(species: dict) -> dict:
    data = pokeapi_get(f"pokemon-species/{species['name']}")

    chinese_name = None

    for name_info in data["names"]:
        if name_info["language"]["name"] == "zh-Hant":
            chinese_name = name_info["name"]
            break

    return {
        "id": data["id"],
        "en": species["name"],
        "zh": chinese_name,
    }

多線程取得第一世代中英文清單:

from langchain.tools import tool
from functools import lru_cache
from concurrent.futures import ThreadPoolExecutor, as_completed

@tool
@lru_cache
def get_all_pokemon_names() -> list[dict]:
    """
    取得第一世代所有 Pokémon 的中英文名稱對照表。

    當需要將中文 Pokémon 名稱轉成 PokéAPI 使用的英文名稱,
    或需要查看第一世代有哪些 Pokémon 時使用。
    """

    data = pokeapi_get("generation/1")
    species_list = data["pokemon_species"]

    pokemon_list = []

    with ThreadPoolExecutor(max_workers=8) as executor:
        futures = [
            executor.submit(
                get_species_name,
                species,
            )
            for species in species_list
        ]

        for future in as_completed(futures):
            pokemon_list.append(
                future.result()
            )

    pokemon_list.sort(key=lambda x: x["id"])

    return pokemon_list

Tool 2 - 取得指定 Pokémon 的基本資料

@tool
@lru_cache
def get_pokemon(name: str) -> dict:
    """
    取得指定 Pokémon 的基本資料。

    適合用來查詢 Pokémon 的屬性與能力值。
    name 必須使用英文名稱,例如 pikachu、charizard。
    """

    data = pokeapi_get(f"pokemon/{name.lower()}")

    if "error" in data:
        return data

    return {
        "name": data["name"],
        "types": [
            item["type"]["name"]
            for item in data["types"]
        ],
        "stats": {
            item["stat"]["name"]: item["base_stat"]
            for item in data["stats"]
        },
    }

Tool 3 - 取得 Pokémon 屬性的剋制與弱點

@tool
@lru_cache
def get_type_info(type_name: str) -> dict:
    """
    取得 Pokémon 屬性的剋制與弱點。

    type_name 必須使用英文,
    例如 water、fire、electric、grass。
    """

    data = pokeapi_get(f"type/{type_name.lower()}")

    if "error" in data:
        return data

    relations = data["damage_relations"]

    return {
        "type": data["name"],
        "double_damage_to": [
            item["name"]
            for item in relations["double_damage_to"]
        ],
        "double_damage_from": [
            item["name"]
            for item in relations["double_damage_from"]
        ],
        "half_damage_to": [
            item["name"]
            for item in relations["half_damage_to"]
        ],
        "half_damage_from": [
            item["name"]
            for item in relations["half_damage_from"]
        ],
        "no_damage_to": [
            item["name"]
            for item in relations["no_damage_to"]
        ],
        "no_damage_from": [
            item["name"]
            for item in relations["no_damage_from"]
        ],
    }

Tool 4 - 取得指定屬性的 Pokémon 候選名單

@tool
@lru_cache
def get_pokemon_by_type(
    type_name: str,
    limit: int = 10,
) -> dict:
    """
    取得指定屬性的 Pokémon 候選名單。

    當需要根據某個屬性挑選、推薦或組隊 Pokémon 時,
    必須先使用此 Tool 取得候選名單,
    不要直接依賴模型記憶提出 Pokémon 候選。
    
    type_name 必須使用英文,例如 electric、grass。

    limit 控制最多回傳多少隻 Pokémon。
    """

    data = pokeapi_get(f"type/{type_name.lower()}")

    if "error" in data:
        return data

    limit = max(1, min(limit, 30))

    pokemon = [
        item["pokemon"]["name"]
        for item in data["pokemon"][:limit]
    ]

    return {
        "type": data["name"],
        "count": len(data["pokemon"]),
        "returned": len(pokemon),
        "pokemon": pokemon,
    }

Agents

根據 LangChain 對 Agents 的說明,Agent 可以理解成 Model + Harness。Harness 是什麼就不多廢話,可以看李弘毅教授的影片。反正 create_agent 就是在 Model 外面包上一套可設定的 Harness,包含 Tools、Prompt、State、Memory、Middleware 等能力;底層核心則是一個 ReAct-style 的循環:

Model → Tool → Model → Tool → … → Finish

總之今天仍然先做一個最基本的 agent,也就是 model + tools,再加 system_prompt。

from langchain.agents import create_agent

agent = create_agent(
    model=model,
    tools=[
        get_all_pokemon_names,
        get_pokemon,
        get_type_info,
        get_pokemon_by_type,
    ],
    system_prompt="""
你是一個 Pokémon 組隊助手。

你的工作是根據使用者的需求,
查詢 PokéAPI 並提供 Pokémon 組隊建議。

規則:

1. 不要依賴自己的記憶猜測 Pokémon 的屬性、能力值或剋制關係,請透過 Tool 查資料再回答。
2. 使用 get_all_pokemon_names 取得第一世代寶可夢中英文資訊。
3. 使用 get_pokemon 先查對手的實際屬性。
3. 使用 get_type_info 查詢該屬性的弱點。
4. 使用 get_pokemon_by_type 取得候選 Pokémon。
5. 只能從 Tool 回傳的候選名單中挑選。
6. 再用 get_pokemon 驗證最後候選的詳細資料。
7. 最後使用繁體中文回答。
8. 清楚說明你推薦 Pokémon 的理由,以及使用到的屬性剋制關係。
""",
)

執行看看,這道館真的是夠隨機的了:

question="""
我要挑戰一個自創道館,道館會派出:

道館會派出以下 Pokémon:
- 鐵甲貝
- 椰蛋樹
- 三合一磁怪

請先查詢這些 Pokémon 的屬性與弱點,
再幫我從第一世代 Pokémon 中,依據屬性剋制關係,推薦 3 隻適合出戰的 Pokémon。
"""

result = agent.invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": question,
            }
        ]
    }
)

Tool Calls 歷程:

Tool Calls:
  → get_all_pokemon_names({})
Tool Calls:
  → get_pokemon({'name': 'cloyster'})
  → get_pokemon({'name': 'exeggutor'})
  → get_pokemon({'name': 'magneton'})
Tool Calls:
  → get_type_info({'type_name': 'water'})
  → get_type_info({'type_name': 'ice'})
  → get_type_info({'type_name': 'grass'})
  → get_type_info({'type_name': 'psychic'})
  → get_type_info({'type_name': 'electric'})
  → get_type_info({'type_name': 'steel'})
Tool Calls:
  → get_pokemon_by_type({'type_name': 'electric', 'limit': 20})
  → get_pokemon_by_type({'type_name': 'bug', 'limit': 20})
  → get_pokemon_by_type({'type_name': 'ground', 'limit': 20})
Tool Calls:
  → get_pokemon({'name': 'jolteon'})
  → get_pokemon({'name': 'pinsir'})
  → get_pokemon({'name': 'dugtrio'})
  → get_type_info({'type_name': 'bug'})
  → get_type_info({'type_name': 'ground'})

最終回答:

推薦隊伍:**雷伊布+凱羅斯+三地鼠**,分別主攻鐵甲貝、椰蛋樹與三合一磁怪。

以下已查詢對手屬性、屬性相剋、候選名單,並再次驗證三隻推薦寶可夢的資料。**採用 PokéAPI 現行屬性規則,隊員限定第一世代寶可夢**,不是初代遊戲的舊制規則;因此三合一磁怪按「電/鋼」計算。

## 一、道館寶可夢的屬性與弱點

雙屬性的受傷倍率需要相乘,不能只看其中一個屬性。

| 對手 | 查得屬性 | 受到 4 倍傷害 | 受到 2 倍傷害 |
|---|---|---|---|
| 鐵甲貝 | 水/冰 | 無 | 電、草、格鬥、岩石 |
| 椰蛋樹 | 草/超能力 | **蟲** | 火、冰、毒、飛行、幽靈、惡 |
| 三合一磁怪 | 電/鋼 | **地面** | 火、格鬥 |

**幾個重要細節:**
- **鐵甲貝不弱火、也不弱鋼**:冰的弱點會被水的抗性抵銷,兩者皆為 `2 × ½=1 倍`。
- **椰蛋樹四倍弱蟲**:草與超能力都弱蟲,合計 `2 × 2=4 倍`。
- **三合一磁怪四倍弱地面**:電與鋼都弱地面,合計 `2 × 2=4 倍`。

## 二、推薦出戰的 3 隻寶可夢

### 1. 雷伊布(Jolteon)——主攻鐵甲貝
**屬性:電|特攻種族值:110|速度種族值:130**

- 電屬性招式打水/冰的鐵甲貝為 **2 倍傷害**。
- 鐵甲貝的**防禦高達 180,但特防只有 45**,因此讓雷伊布使用**電屬性特殊招式**,比從物理面強攻更合理。
- 雷伊布速度種族值 130,高於鐵甲貝的 70,在等級與培育條件相近時有先手優勢。

**使用重點:**主要負責鐵甲貝;不要以電招硬打三合一磁怪,對方的電屬性使其只受到 **½ 倍**電傷害。

### 2. 凱羅斯(Pinsir)——主攻椰蛋樹
**屬性:蟲|攻擊種族值:125|速度種族值:85**

- 蟲屬性招式對椰蛋樹造成 **4 倍傷害**,是其最明顯的屬性破口。
- 凱羅斯攻擊種族值 125,適合使用**蟲屬性物理招式**。
- 速度種族值 85,高於椰蛋樹的 55,在相近條件下較有機會先攻。
- 純蟲屬性受到草招僅 **½ 倍傷害**,但受到超能力招式仍是 **1 倍**,不能把它當成全面抵抗椰蛋樹。

**使用重點:**務必配置所玩版本中可合法取得的蟲屬性攻擊招式;**自身是蟲屬性,不代表所有招式都能打出四倍傷害**。

### 3. 三地鼠(Dugtrio)——主攻三合一磁怪
**屬性:地面|攻擊種族值:100|速度種族值:120**

- 地面屬性招式對三合一磁怪造成 **4 倍傷害**。
- 地面屬性**免疫電屬性招式**,能在預判電招時提供換入機會。
- 速度種族值 120,高於三合一磁怪的 70;配合物理地面招式,適合快速進攻。

**使用重點:**三地鼠 HP 種族值只有 35、防禦 50,不宜隨意承受攻擊。它也弱水、冰與草,**避免拿它硬接鐵甲貝或椰蛋樹**。

## 三、建議對位

| 我方 | 主要對手 | 進攻方向 | 屬性倍率 |
|---|---|---|---|
| **雷伊布** | 鐵甲貝 | 電屬性特殊招式,攻擊低特防 | **2 倍** |
| **凱羅斯** | 椰蛋樹 | 蟲屬性物理招式 | **4 倍** |
| **三地鼠** | 三合一磁怪 | 地面屬性物理招式 | **4 倍** |

這套隊伍的優點是**分工明確,三隻都有對應的有利對位**。上述倍率僅指屬性相剋,尚未包含同屬性招式加成;實戰仍須考慮等級、招式、特性與道具,不能保證先手或一擊擊倒。

不過目前這個 Agent 只有考慮屬性關係而已,沒有考慮種族值或技能,而且只認識第一世代,距離成為真正的寶可夢大師還非常遙遠啊。

每日一句

野生的 Tool 出現了,但 Agent 決定要不要理它。


上一篇
可是老闆,那不是 Agent
下一篇
用 LangGraph 幫 Pokémon Agent 畫路
系列文
Data Engineer 下班後偷學 AI8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言